package org.github.manx98.jlibhac;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.github.manx98.jlibhac.common.keys.KeySet;
import org.github.manx98.jlibhac.common.math.IByteUtil;
import org.github.manx98.jlibhac.fs.BinaryReader;
import org.github.manx98.jlibhac.fs.MemoryStorage;
import org.github.manx98.jlibhac.fs.fsa.IFile;
import org.github.manx98.jlibhac.fs.fsa.IFileSystem;
import org.github.manx98.jlibhac.fs.fsa.OpenMode;
import org.github.manx98.jlibhac.fssystem.NcaContentType;
import org.github.manx98.jlibhac.fssystem.NcaSectionType;
import org.github.manx98.jlibhac.fssystem.PartitionFileSystem;
import org.github.manx98.jlibhac.hle.Npdm;
import org.github.manx98.jlibhac.ns.ApplicationControlProperty;
import org.github.manx98.jlibhac.ns.ApplicationTitle;
import org.github.manx98.jlibhac.ns.Language;
import org.github.manx98.jlibhac.tools.fs.Xci;
import org.github.manx98.jlibhac.tools.fs.XciPartitionType;
import org.github.manx98.jlibhac.tools.fssystem.IntegrityCheckLevel;
import org.github.manx98.jlibhac.tools.fssystem.ncautils.Nca;
import org.github.manx98.jlibhac.tools.fssystem.ncautils.TitleVersion;
import org.github.manx98.jlibhac.tools.ncm.Cnmt;
import org.github.manx98.jlibhac.tools.ncm.ContentMetaType;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
public class ApplicationLibrary {
    /**
     * 加载程序文件
     *
     * @param keySet          密匙对象
     * @param applicationFile 程序文件
     */
    public static GameInformation loadApplication(KeySet keySet, String applicationFile) throws IOException {
        String extension = getExtension(applicationFile).toLowerCase();
        if (
                !(
                        extension.equals(".nsp") ||
                                extension.equals(".pfs0") ||
                                extension.equals(".xci") ||
                                extension.equals(".nca") ||
                                extension.equals(".nro") ||
                                extension.equals(".nso") ||
                                extension.equals(".nsz") ||
                                extension.equals(".xcz")
                )
        ) {
            throw new IllegalArgumentException("Unsupported file extension: " + extension);
        }
        AtomicBoolean hasMainNca = new AtomicBoolean(false);
        try (BinaryReader file = new BinaryReader(applicationFile)) {
            if (extension.equals(".nsp") ||
                    extension.equals(".pfs0") ||
                    extension.equals(".xci") ||
                    extension.equals(".nsz") ||
                    extension.equals(".xcz")
            ) {
                GameInformation gameInformation = new GameInformation();
                PartitionFileSystem pfs;
                AtomicBoolean isExeFs = new AtomicBoolean(false);
                if (extension.equals(".xci") || extension.equals(".xcz")) {
                    Xci xci = new Xci(keySet, file.asStorage());
                    pfs = xci.openPartition(XciPartitionType.Secure);
                } else {
                    pfs = new PartitionFileSystem(file.asStorage());
                    pfs.enumerateEntries("/", ".*", fileEntry -> {
                        String fileEntryExtension = getExtension(fileEntry.getPath());
                        if (fileEntryExtension.equalsIgnoreCase(".nca") || fileEntryExtension.equalsIgnoreCase(".ncz")) {
                            try (IFile ncaFile = pfs.doOpenFile(fileEntry.getPath(), OpenMode.Read)) {
                                log.info("成功打开NCA文件：{}", fileEntry.getPath());
                                Nca nca = new Nca(keySet, ncaFile.asStorage());
//                                int dataIndex = Nca.getSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
                                // Some main NCAs don't have a data partition, so check if the partition exists before opening it
//                                if (nca.header.getContentType() == NcaContentType.Program && !(nca.sectionExists(NcaSectionType.Data) && nca.header.getFsHeader(dataIndex).isPatchSection())) {
                                if (nca.header.getContentType() == NcaContentType.Program && nca.sectionExists(NcaSectionType.Data)) {
                                    hasMainNca.set(true);
                                    return false;
                                } else if (getFileNameWithoutExtension(fileEntry.getPath()).equals("main")) {
                                    isExeFs.set(true);
                                }
                            }
                        }
                        return true;
                    });
                    if (!hasMainNca.get() && !isExeFs.get()) {
                        fillContentMetaInfo(keySet, pfs, gameInformation);
                        log.info("application is not has main or exeFs: {}", applicationFile);
                        return gameInformation;
                    }
                }
                fillContentMetaInfo(keySet, pfs, gameInformation);
                if (isExeFs.get()) {
                    try (IFile npdmFile = pfs.doOpenFile("/main.npdm", OpenMode.Read)) {
                        Npdm npdm = new Npdm(npdmFile.asStorage());
                        gameInformation.titleName = npdm.TitleName;
                        gameInformation.titleId = formatId(npdm.Aci0.TitleId.toString(16), 16);
                        return gameInformation;
                    }
                } else {
                    ControlFsAndTitleId controlFsAndTitleId = getControlFsAndTitleId(keySet, pfs);
                    String titleId = controlFsAndTitleId.titleId;
                    log.info("titleId: {}", titleId);
                    IFileSystem controlFs = controlFsAndTitleId.controlFs;
                    byte[] controlHolder = readControlData(controlFs);
                    fillGameInformation(controlHolder, gameInformation);
                    Map<Language, byte[]> iconData = new HashMap<>();
                    controlFs.enumerateEntries("/", "icon_.*\\.dat", entry -> {
                        log.debug("发现图标文件 {}", entry.getPath());
                        String name = entry.getName();
                        Language language = Language.formName(name.substring(name.indexOf("_") + 1, name.indexOf(".")));
                        try (IFile icon = controlFs.doOpenFile(entry.getPath(), OpenMode.Read)) {
                            iconData.put(language, icon.readBytes((int) icon.getSize()));
                        }
                        return true;
                    });
                    for (TitleInfo titleInfo : gameInformation.getTitleInfoList()) {
                        titleInfo.icon = iconData.get(titleInfo.language);
                    }
                    return gameInformation;
                }
            } else {
                throw new IllegalArgumentException("extension " + extension + " is not supported!");
            }
        }
    }

    public static void fillContentMetaInfo(KeySet keySet, IFileSystem pfs, GameInformation gameInformation) throws IOException {
        Cnmt cnmt = loadGameCnmt(keySet, pfs);
        log.info("成功加载Cnmt信息:{}", cnmt);
        gameInformation.titleVersion = cnmt.TitleVersion.Version.toString();
        gameInformation.type = cnmt.Type;
        gameInformation.metaTitleId = formatId(cnmt.getTitleId().toString(16), 16);
        gameInformation.titleId = formatId(cnmt.getApplicationTitleId().toString(16), 16);
        TitleVersion minimumApplicationVersion = cnmt.getMinimumApplicationVersion();
        if (minimumApplicationVersion != null) {
            gameInformation.minimumApplicationVersion = minimumApplicationVersion.toString();
        }
        TitleVersion minimumSystemVersion = cnmt.getMinimumSystemVersion();
        if (minimumSystemVersion != null) {
            gameInformation.minimumSystemVersion = minimumSystemVersion.toString();
        }
    }

    public static Cnmt loadGameCnmt(KeySet keySet, IFileSystem fs) throws IOException {
        Cnmt[] cnmtRef = new Cnmt[1];
        fs.enumerateEntries("/", ".*\\.cnmt.nca", entry -> {
            try (IFile ncaFile = fs.doOpenFile(entry.getPath(), OpenMode.Read)) {
                Nca nca = new Nca(keySet, ncaFile.asStorage());
                if (nca.header.getContentType() == NcaContentType.Meta) {
                    IFileSystem pfs0 = nca.openFileSystem(0, IntegrityCheckLevel.None);
                    pfs0.enumerateEntries("/", ".*", mateEntry -> {
                        try (IFile file = pfs0.doOpenFile(mateEntry.getPath(), OpenMode.Read)) {
                            cnmtRef[0] = new Cnmt(file.asStorage());
                        }
                        return false;
                    });
                    if (cnmtRef[0] != null && cnmtRef[0].getType() == ContentMetaType.Application) {
                        return false;
                    }
                } else {
                    log.warn("{} is not meta content", entry.getPath());
                }
            }
            return true;
        });
        if (cnmtRef[0] != null) {
            return cnmtRef[0];
        }
        throw new IllegalArgumentException("Cannot open meta nca file!");
    }

    private static void fillGameInformation(byte[] controlHolder, GameInformation gameInformation) throws IOException {
        ApplicationControlProperty applicationControlProperty = (ApplicationControlProperty) IByteUtil.mapType(ApplicationControlProperty.class, new MemoryStorage(controlHolder), controlHolder.length).value;
        List<TitleInfo> titleInfoList = gameInformation.getTitleInfoList();
        for (int i = 0; i < applicationControlProperty.Title.length; i++) {
            ApplicationTitle applicationTitle = applicationControlProperty.Title[i];
            if (applicationTitle.Name != null && !applicationTitle.Name.isBlank()) {
                titleInfoList.add(new TitleInfo(applicationTitle.Name, Language.valueOf(i), applicationTitle.Publisher, null));
            }
        }
        gameInformation.version = applicationControlProperty.getDisplayVersionString();
    }

    @Data
    @AllArgsConstructor
    public static class TitleInfo {
        String name;
        Language language;
        String publisher;
        byte[] icon;
    }

    @Data
    public static class GameInformation {
        public List<TitleInfo> titleInfoList = new ArrayList<>();
        public ContentMetaType type;
        public String titleVersion;
        public String metaTitleId;
        String titleName;
        String publisher;
        String version;
        String titleId;
        byte[] applicationIcon;
        String minimumSystemVersion;
        String minimumApplicationVersion;
    }

    private static byte[] readControlData(IFileSystem controlFs) throws IOException {
        try (IFile controlFile = controlFs.doOpenFile("/control.nacp", OpenMode.Read)) {
            log.info("成功打开 /control.nacp， fileSize=" + controlFile.getSize());
            return controlFile.readBytes((int) controlFile.getSize());
        }
    }

    private static ControlFsAndTitleId getControlFsAndTitleId(KeySet keySet, PartitionFileSystem pfs) throws IOException {
        GameDataResult ncaList = getGameData(keySet, pfs, 0);
        // Return the ControlFS
        return new ControlFsAndTitleId(
                ncaList.controlNca.openFileSystemByType(NcaSectionType.Data, IntegrityCheckLevel.None),
                formatId(ncaList.controlNca.header.getTitleId().toString(16), 16)
        );
    }

    static class GameDataResult {
        public Nca mainNca;
        public Nca patchNca;
        public Nca controlNca;
    }

    /**
     * @param keySet       密匙对象
     * @param pfs          文件系统
     * @param programIndex 程序索引
     */
    private static GameDataResult getGameData(KeySet keySet, PartitionFileSystem pfs, int programIndex) throws IOException {
        final GameDataResult gameData = new GameDataResult();
        keySet.importTickets(pfs);
        pfs.enumerateEntries("/", ".*\\.nca", fileEntry -> {
            try (IFile ncaFile = pfs.doOpenFile(fileEntry.getPath(), OpenMode.Read)) {
                Nca nca = new Nca(keySet, ncaFile.asStorage());
// 在非标准的文件中该校验可能会导致意外的错误
//                int ncaProgramIndex = nca.header.getTitleId().and(new Uint16(0xF)).intValue();
//                if (ncaProgramIndex != programIndex) {
//                    return true;
//                }
                if (nca.header.getContentType() == NcaContentType.Program) {
                    int dataIndex = Nca.getSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);

                    if (nca.sectionExists(NcaSectionType.Data) && nca.header.getFsHeader(dataIndex).isPatchSection()) {
                        gameData.patchNca = nca;
                    } else {
                        gameData.mainNca = nca;
                    }
                } else if (nca.header.getContentType() == NcaContentType.Control) {
                    gameData.controlNca = nca;
                }
            }
            return true;
        });
        return gameData;
    }

    static class ControlFsAndTitleId {
        public final IFileSystem controlFs;
        public final String titleId;

        public ControlFsAndTitleId(IFileSystem controlFs, String titleId) {
            this.controlFs = controlFs;
            this.titleId = titleId;
        }
    }

    private static String getFileNameWithoutExtension(String path) {
        int sep = path.lastIndexOf('/');
        int ext = path.lastIndexOf('.');
        if (sep == -1) {
            if (ext == -1) {
                return path;
            } else {
                return path.substring(0, ext);
            }
        } else {
            if (ext == -1) {
                return path.substring(sep + 1);
            } else {
                return path.substring(sep + 1, ext);
            }
        }
    }

    /**
     * 获取文件拓展名
     *
     * @param filePath 文件路径
     * @return 拓展名
     */
    public static String getExtension(String filePath) {
        return filePath.substring(filePath.lastIndexOf("."));
    }

    private static String formatId(String id, int length) {
        return "0".repeat(length - id.length()) + id;
    }
}
